package scanner;

import compiler.Properties;
import compiler.SubCCompiler;

import parser.GrammarSymbols;
import util.Arquivo;

/**
 * Scanner class
 * @version 2011-september
 * @discipline Compiladores
 * @author 
 * 	Gustavo H P Carvalho
 * 	Anderson de Olivera
 * 	Rodrigo Lira
 * 	Sergio Ribeiro
 * @email 
 * 	gustavohpcarvalho@ecomp.poli.br
 * 	aom@ecomp.poli.br
 * 	rcls@ecomp.poli.br
 * 	sfr@ecomp.poli.br
 */
public class Scanner {

	// The file object that will be used to read the source code
	private Arquivo file;
	// The last char read from the source code
	private char currentChar;
	// The kind of the current token
	private int currentKind;
	// Buffer to append characters read from file
	private StringBuffer currentSpelling;
	// Current line and column in the source file
	private int line, column;
	
	/**
	 * Default constructor
	 */
	public Scanner() {
		this.file = new Arquivo(Properties.sourceCodeLocation);		
		this.line = 0;
		this.column = 0;
		this.currentChar = this.file.readChar();
	}
	
	/**
	 * Returns the next token
	 * @return
	 */ //TODO Done!
	public Token getNextToken() throws LexicalException {
			// Initializes the string buffer
			// Ignores separators
			// Clears the string buffer
			// Scans the next token
		// Creates and returns a token for the lexema identified
		
		this.currentSpelling = new StringBuffer("");
		
		while(isSeparator(this.currentChar)){
			this.scanSeparator();
		}
		
		this.currentSpelling = new StringBuffer("");
		this.currentKind = this.scanToken();
		
		Token token = new Token(this.currentKind, this.currentSpelling.toString(), this.line, this.column);
		return token;
	}
	
	/**
	 * Returns if a character is a separator
	 * @param c
	 * @return
	 */
	private boolean isSeparator(char c) {
		if ( c == '#' || c == ' ' || c == '\n' || c == '\t' ) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Reads (and ignores) a separator
	 * @throws LexicalException
	 */ //TODO Done!
	private void scanSeparator() throws LexicalException {
		// If it is a comment line
			// Gets next char
			// Reads characters while they are graphics or '\t'
			// A command line should finish with a \n
		
		if(this.currentChar == '#'){
			this.getNextChar();
			while(this.isGraphic(this.currentChar) || this.currentChar == '\t'){
				this.getNextChar();
			}
		}
		else{
			this.getNextChar();
		}
	}
	
	/**
	 * Gets the next char
	 */
	private void getNextChar() {
		// Appends the current char to the string buffer
		this.currentSpelling.append(this.currentChar);
		// Reads the next one
		this.currentChar = this.file.readChar();
		// Increments the line and column
		this.incrementLineColumn();
	}
	
	/**
	 * Increments line and column
	 */
	private void incrementLineColumn() {
		// If the char read is a '\n', increments the line variable and assigns 0 to the column
		if ( this.currentChar == '\n' ) {
			this.line++;
			this.column = 0;
		// If the char read is not a '\n' 
		} else {
			// If it is a '\t', increments the column by 4
			if ( this.currentChar == '\t' ) {
				this.column = this.column + 4;
			// If it is not a '\t', increments the column by 1
			} else {
				this.column++;
			}
		}
	}
	
	/**
	 * Returns if a char is a digit (between 0 and 9)
	 * @param c
	 * @return
	 */
	private boolean isDigit(char c) {
		if ( c >= '0' && c <= '9' ) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Returns if a char is a letter (between a and z or between A and Z)
	 * @param c
	 * @return
	 */
	private boolean isLetter(char c) {
		if ( (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Returns if a char is a graphic (any ASCII visible character)
	 * @param c
	 * @return
	 */
	private boolean isGraphic(char c) {
		if ( c >= ' ' && c <= '~' ) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Scans the next token
	 * Simulates the DFA that recognizes the language described by the lexical grammar
	 * @return
	 * @throws LexicalException
	 */ //TODO Done!
	private int scanToken() throws LexicalException {
		// The initial automata state is 0
		// While loop to simulate the automata
		//System.out.print(this.currentChar);
		if (isDigit(this.currentChar)) {
			// If the first character is a digit, then the token must be a number [digits(.digits)?].
			return initialIsDigit();
		}
		else if (isLetter(this.currentChar)) {
			// If the first character is a letter, then the token must be a identifier [letter ( letter | digit )*]
			// or keyWord [void | int | double | boolean | if | else | while | return | break | continue | println | false | true].
			return initialIsLetter();
		}
		else if (isGraphic(this.currentChar)) {
			// If the first character is a graphic, then the token must be a graphic
			// [ ; | ( | ) | { | } | = | * | / | + | - | == | != | < | <= | > | >= | , ].
			return initialIsGraphic(); 
		}
		
		return GrammarSymbols.EOT;
	}
	
	/**
	 * If the first character is a digit, then the token must be a number [digits(.digits)?].
	 * @return
	 * @throws LexicalException
	 */
	private int initialIsDigit() throws LexicalException {
		
		int kindToken = GrammarSymbols.NUMBER;
		while (isDigit(this.currentChar)) {
			this.getNextChar();
			
			if (this.currentChar == '.') {
				this.getNextChar();
				if (isDigit(this.currentChar)) {
					while(isDigit(this.currentChar)) this.getNextChar();
				}
				else {
					// TODO: Check exception message.
					throw new LexicalException("isDigit", this.currentChar, this.line, this.column);
				}
			}
		}
		
		if (!isSeparator(this.currentChar) && !isSpecialChars()) {
			// TODO: Check exception message.
			throw new LexicalException("isDigit", this.currentChar, this.line, this.column);
		}
		
		return kindToken;
	}
	
	private boolean isSpecialChars() {
		
		return
				this.currentChar == ';' ||
				this.currentChar == '(' ||
				this.currentChar == ')' ||
				this.currentChar == '{' ||
				this.currentChar == '}' ||
				this.currentChar == '=' ||
				this.currentChar == '*' ||
				this.currentChar == '/' ||
				this.currentChar == '+' ||
				this.currentChar == '-' ||
				this.currentChar == '=' ||
				this.currentChar == '!' ||
				this.currentChar == '<' ||
				this.currentChar == '>' ||
				this.currentChar == ',';
	}
	
	/**
	 * If the first character is a letter, then the token must be a identifier [letter ( letter | digit )*]
	 * or keyWord [void | int | double | boolean | if | else | while | return | break | continue | println | false | true].
	 * @return
	 * @throws LexicalException
	 */
	private int initialIsLetter() throws LexicalException {
		
		while (isLetter(this.currentChar) || isDigit(this.currentChar)) {
			this.getNextChar();
		}
		
		if (!isSeparator(this.currentChar) && !isSpecialChars()) {
			// TODO: Check exception message.
			throw new LexicalException("isLetter", this.currentChar, this.line, this.column);
		}
		
		return checkKeyWords();
	}
	
	private int checkKeyWords() {
		
		String keyWord = this.currentSpelling.toString();
		
		if (keyWord.equals("void"))
			return GrammarSymbols.VOID;
		else if (keyWord.equals("int"))
			return GrammarSymbols.INT;
		else if (keyWord.equals("double"))
			return GrammarSymbols.DOUBLE;
		else if (keyWord.equals("boolean"))
			return GrammarSymbols.BOOLEAN;
		else if (keyWord.equals("if"))
			return GrammarSymbols.IF;
		else if (keyWord.equals("else"))
			return GrammarSymbols.ELSE;
		else if (keyWord.equals("while"))
			return GrammarSymbols.WHILE;
		else if (keyWord.equals("return"))
			return GrammarSymbols.RETURN;
		else if (keyWord.equals("break"))
			return GrammarSymbols.BREAK;
		else if (keyWord.equals("continue"))
			return GrammarSymbols.CONTINUE;
		else if (keyWord.equals("println"))
			return GrammarSymbols.PRINTLN;
		else if (keyWord.equals("false"))
			return GrammarSymbols.FALSE;
		else if (keyWord.equals("true"))
			return GrammarSymbols.TRUE;
		
		return GrammarSymbols.ID;
	}
	
	/**
	 * If the first character is a graphic, then the token must be a graphic
	 * [ ; | ( | ) | { | } | = | * | / | + | - | == | != | < | <= | > | >= | , ].
	 * @return
	 * @throws LexicalException
	 */
	private int initialIsGraphic() throws LexicalException {
		
		switch (this.currentChar) {
			case ';':
				this.getNextChar();
				return GrammarSymbols.SEMICOLON;
			case '(':
				this.getNextChar();
				return GrammarSymbols.LPAR;
			case ')':
				this.getNextChar();
				return GrammarSymbols.RPAR;
			case '{':
				this.getNextChar();
				return GrammarSymbols.LBRACKET;
			case '}':
				this.getNextChar();
				return GrammarSymbols.RBRACKET;
			case '=':
				this.getNextChar();
				if (this.currentChar == '=') {
					this.getNextChar();
					return GrammarSymbols.EQUAL;
				}
				else
					return GrammarSymbols.ASSIGN;
			case '*':
				this.getNextChar();
				return GrammarSymbols.MULT;
			case '/':
				this.getNextChar();
				return GrammarSymbols.DIV;
			case '+':
				this.getNextChar();
				return GrammarSymbols.PLUS;
			case '-':
				this.getNextChar();
				return GrammarSymbols.MINUS;
			case '!':
				this.getNextChar();
				if (this.currentChar == '=') {
					this.getNextChar();
					return GrammarSymbols.NOTEQUAL;
				}
				else {
					// TODO: Check exception message.
					throw new LexicalException("isGraphic", this.currentChar, this.line, this.column);
				}
			case '<':
				this.getNextChar();
				if (this.currentChar == '=') {
					this.getNextChar();
					return GrammarSymbols.LESSEREQUALTHAN;
				}
				else
					return GrammarSymbols.LESSERTHAN;
			case '>':
				this.getNextChar();
				if (this.currentChar == '=') {
					this.getNextChar();
					return GrammarSymbols.GREATEREQUALTHAN;
				}
				else
					return GrammarSymbols.GREATERTHAN;
			case ',':
				this.getNextChar();
				return GrammarSymbols.COMMA;
		}
		
		// TODO: Check exception message.
		throw new LexicalException("isGraphic", this.currentChar, this.line, this.column);
	}
}
